ποΈGitΠ―ΡΠ°ποΈ
Commit c7fbc6e9d3e9fdd2d57de9ac22a8d088651c6fbd
Parents : ad61441
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-05-30T08:01:29-07:00
Committer : GitHub <noreply@github.com>
Date : 2026-05-30T15:01:29Z
fix: address top Crashlytics crashes in beta 2.7.14 (#5672)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Changes
6 files changed, 66 insertions(+), 19 deletions(-)
Diff
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
index a3b42260d1..af8704d542 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
@@ -17,14 +17,12 @@
package org.meshtastic.app.map.component
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
-import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
@@ -54,15 +52,18 @@ fun NodeClusterMarkers(
// If that view is not attached to the hierarchy (which it often isn't during rendering),
// it fails to find the Lifecycle and SavedState owners. We propagate them to the root view
// so the internal snapshot view can find them when walking up the tree.
- // We do this in a SideEffect to ensure it happens before or during composition of children.
- SideEffect {
+ // DisposableEffect runs at composition time (not post-composition like SideEffect),
+ // ensuring owners are set before the Clustering composable triggers marker rendering.
+ DisposableEffect(lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
- if (root.findViewTreeLifecycleOwner() == null) {
- root.setViewTreeLifecycleOwner(lifecycleOwner)
- }
- if (root.findViewTreeSavedStateRegistryOwner() == null) {
- root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
+ root.setViewTreeLifecycleOwner(lifecycleOwner)
+ root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
+ // Also set on the view itself in case the internal renderer walks from a child
+ if (view !== root) {
+ view.setViewTreeLifecycleOwner(lifecycleOwner)
+ view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
}
+ onDispose {}
}
Clustering(
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/NumberFormatter.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/NumberFormatter.kt
index e76e7c029b..0a65a7dc03 100644
--- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/NumberFormatter.kt
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/NumberFormatter.kt
@@ -23,13 +23,17 @@ import kotlin.math.roundToLong
object NumberFormatter {
/** Formats a double value with the specified number of decimal places. */
fun format(value: Double, decimalPlaces: Int): String {
+ if (value.isNaN() || value.isInfinite()) return "β"
val factor = 10.0.pow(decimalPlaces)
val rounded = (value * factor).roundToLong()
return formatFixedPoint(rounded, decimalPlaces)
}
/** Formats a float value with the specified number of decimal places. */
- fun format(value: Float, decimalPlaces: Int): String = format(value.toDouble(), decimalPlaces)
+ fun format(value: Float, decimalPlaces: Int): String {
+ if (value.isNaN() || value.isInfinite()) return "β"
+ return format(value.toDouble(), decimalPlaces)
+ }
private fun formatFixedPoint(scaledValue: Long, decimalPlaces: Int): String {
if (decimalPlaces == 0) return scaledValue.toString()
diff --git a/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/NumberFormatterTest.kt b/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/NumberFormatterTest.kt
index 041ed91fa8..29c61fde05 100644
--- a/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/NumberFormatterTest.kt
+++ b/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/NumberFormatterTest.kt
@@ -35,4 +35,17 @@ class NumberFormatterTest {
assertEquals("1", NumberFormatter.format(1.23, 0))
assertEquals("-1", NumberFormatter.format(-1.23, 0))
}
+
+ @Test
+ fun testFormatNaN() {
+ assertEquals("β", NumberFormatter.format(Double.NaN, 2))
+ assertEquals("β", NumberFormatter.format(Float.NaN, 1))
+ }
+
+ @Test
+ fun testFormatInfinity() {
+ assertEquals("β", NumberFormatter.format(Double.POSITIVE_INFINITY, 2))
+ assertEquals("β", NumberFormatter.format(Double.NEGATIVE_INFINITY, 2))
+ assertEquals("β", NumberFormatter.format(Float.POSITIVE_INFINITY, 1))
+ }
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt
index 6504faf80f..60f2310c39 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt
@@ -51,7 +51,13 @@ class StoreForwardPacketHandlerImpl(
override fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
val payload = packet.decoded?.payload ?: return
- val u = StoreAndForward.ADAPTER.decode(payload)
+ val u =
+ try {
+ StoreAndForward.ADAPTER.decode(payload)
+ } catch (e: IOException) {
+ Logger.e(e) { "Failed to parse StoreAndForward packet" }
+ return
+ }
handleReceivedStoreAndForward(dataPacket, u, myNodeNum)
}
diff --git a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt
index 9bf237733d..702f53eebe 100644
--- a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt
+++ b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt
@@ -338,4 +338,18 @@ class StoreForwardPacketHandlerImplTest {
verifySuspend { packetRepository.updateSFPPStatus(any(), any(), any(), any(), any(), any(), any()) }
}
+
+ // ---------- Legacy S&F: malformed proto ----------
+
+ @Test
+ fun `handleStoreAndForward with malformed payload does not crash`() = testScope.runTest {
+ val malformedPayload = ByteString.of(0xFF.toByte(), 0xFE.toByte(), 0x07, 0x0E)
+ val packet =
+ MeshPacket(from = 999, decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = malformedPayload))
+ val dataPacket = makeDataPacket(999)
+
+ // Should not throw β the handler catches the IOException from proto decoding
+ handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
+ advanceUntilIdle()
+ }
}
diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt
index c9f3a904f7..e89abbcb35 100644
--- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt
+++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt
@@ -86,14 +86,23 @@ internal class SerialConnectionImpl(
}
port.open(usbDeviceConnection)
- port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
+ try {
+ port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
- // Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as
- // present and starts its serial-side Meshtastic protocol. Empirically, omitting these
- // signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at
- // Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion.
- port.dtr = true
- port.rts = true
+ // Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as
+ // present and starts its serial-side Meshtastic protocol. Empirically, omitting these
+ // signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at
+ // Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion.
+ port.dtr = true
+ port.rts = true
+ } catch (e: java.io.IOException) {
+ Logger.w(e) { "USB control transfer failed during port setup β device may have disconnected" }
+ closed.set(true)
+ ignoreException(silent = true) { port.close() }
+ closedLatch.countDown()
+ listener.onDisconnected(e)
+ return
+ }
Logger.d { "Starting serial reader thread" }
val io =
Served by rngit 1.5.0 - Generated in 0.1s